Skip to content

vm: render the top-level self as "main", and give remove_method's NameError its receiver - #738

Merged
tannevaled merged 1 commit into
mainfrom
wave53-main-receiver
Sep 28, 2026
Merged

tannevaled merged 1 commit into
mainfrom
wave53-main-receiver

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Two residuals another wave routed out of its own cluster, both measured against ruby 4.0.5 first.

The top-level self renders as main

MRI renders an object that carries a singleton class by its identity string, with exactly one exception: the top-level self is main. name_err_mesg_to_str (error.c ruby_4_0:2679-2689) takes the singleton arm, sets the "an instance of " prefix to empty, and then chooses between rb_any_to_s(obj) and the literal "main" on obj == rb_vm_top_self().

rbgo had the arm and not the exception:

self.nope_xyz
  MRI  : undefined method 'nope_xyz' for main
  rbgo : undefined method 'nope_xyz' for #<Object:0x0000000000000008>

An ordinary object carrying a singleton was already right, which is what said the arm itself was sound and only its exception was missing.

The half of the test that is more interesting than the fix

MRI does not dispatch #inspect or #to_s to build that identity. Overriding both on the receiver changes the message on neither engine:

o = Object.new
def o.inspect; "CUSTOM-INSPECT"; end
def o.to_s;    "CUSTOM-TO-S";    end
o.nope
# MRI  : undefined method 'nope' for #<Object:0x…>
# rbgo : undefined method 'nope' for #<Object:0x…>

So this file's existing choice to construct the string rather than send to the receiver is MRI's choice too, and the test now says so rather than leaving it as a comment. An error path that runs user code can recurse or raise again. Measured in both directions before writing it — I was about to "fix" this by dispatching to_s, and the oracle said no.

remove_method's NameError carries its receiver

Module#remove_method on a name that is not there raises through rb_name_err_raise (vm_method.c, rb_mod_remove_method), whose first argument is the exception's receiver.

class K; end
K.send(:remove_method, :nope)
# MRI  : ["method 'nope' not defined in K", K]
# rbgo : ["method 'nope' not defined in K", nil]

This is one of the 36 bare-raise("NameError", …) sites that the census in exception.go counts (#732). It is fixed because the oracle was measured for it — not because bare raises are wrong in general: the global-variable ones legitimately record no receiver, and #receiver raising ArgumentError there is MRI's own answer.

Non-regression, checked rather than assumed

source before after
Object.new.nope (no singleton) an instance of Object unchanged
a singleton object with overridden inspect/to_s #<Object:0x…> unchanged
a singleton object with no override #<Object:0x…> unchanged
Q.const_get(:Nope) receiver Q unchanged

Full suite green on Go 1.27.1 with CGO_ENABLED=0; wasip1 and js/wasm build; gofmt clean.

🤖 Generated with Claude Code

…eError its receiver

Two residuals another wave routed out of its own cluster, both measured against
ruby 4.0.5 first.

MRI renders an object that carries a singleton class by its identity string, with
exactly one exception: the top-level self is "main". name_err_mesg_to_str
(error.c ruby_4_0:2679-2689) takes the singleton arm, sets the "an instance of "
prefix to empty, and then chooses between rb_any_to_s(obj) and the literal
"main" on `obj == rb_vm_top_self()`. rbgo had the arm and not the exception:

    self.nope_xyz
      MRI  : undefined method 'nope_xyz' for main
      rbgo : undefined method 'nope_xyz' for #<Object:0x0000000000000008>

An ordinary object with a singleton was already right, which is what said the
arm itself was fine and only its exception was missing.

The test also pins what must NOT move, and it is the more interesting half: MRI
does NOT dispatch #inspect or #to_s to build that identity. Overriding both on
the receiver changes the message on neither engine -- measured, both directions
-- so this file's existing choice to construct the string rather than send to
the receiver is MRI's choice too, and the test now says so. An error path that
runs user code can recurse or raise again.

Second: Module#remove_method on a name that is not there raises through
rb_name_err_raise (vm_method.c rb_mod_remove_method), whose first argument IS
the exception's receiver. rbgo built the same message through the bare raise()
form and recorded none, so #receiver answered nil where MRI answers the module.
This is one of the 36 bare-raise NameError sites the census in exception.go
counts; it is fixed because the oracle was measured for it, not because bare
raises are wrong in general -- the global-variable ones legitimately record no
receiver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 977a83b into main Sep 28, 2026
9 checks passed
@tannevaled
tannevaled deleted the wave53-main-receiver branch September 28, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant